#include <stdexcept>
#include <unistd.h>
#include "PhidgetManager.h"
#include "PhidgetPropertyAlias.h"
#include "Phidget.h"
#include "MonkeyRuntime.h"
#include "MonkeyContext.h"
#include "MessageCenter.h"
#include "Message.h"


//
// A device has been attached, notify the manager.
//
static int Attached_Handler(CPhidgetHandle phid, void *userPtr)
{
	PhidgetManager *manager = (PhidgetManager *)userPtr;


	manager->PhidgetDidAttach(phid);

	return true;
}


//
// A device has been detached, notify the manager.
//
static int Detached_Handler(CPhidgetHandle phid, void *userPtr)
{
	PhidgetManager *manager = (PhidgetManager *)userPtr;


	manager->PhidgetDidDetach(phid);

	return true;
}


//
// Create a new phidget manager on the local computer. The PhidgetManager
// allows us to interact with specific phidgets.
//
PhidgetManager::PhidgetManager(MonkeyRuntime *rt)
{
	devices = NULL;
	_serverID = _address = _password = NULL;
	_port = -1;

	if (CPhidgetManager_create(&managerHandle) != 0)
		throw std::runtime_error("Could not create a new phidget manager.");
	
	if (CPhidgetManager_open(managerHandle) != 0)
		throw std::runtime_error("Could not connect to the phidget manager.");
	
	CommonConstruct(rt);
}


//
// Create a new phidget manger and connect to the identified remote server
// for phidget communication.
//
PhidgetManager::PhidgetManager(MonkeyRuntime *rt, const char *serverID, const char *password)
{
	devices = NULL;
	_address = NULL;
	_port = -1;

	if (CPhidgetManager_create(&managerHandle) != 0)
		throw std::runtime_error("Could not create a new phidget manager.");
	
	if (CPhidgetManager_openRemote(managerHandle, serverID, password) != 0)
		throw std::runtime_error("Could not connect to the phidget manager.");
	
	_serverID = new std::string(serverID);
	_password = (password != NULL ? new std::string(password) : NULL);
	
	CommonConstruct(rt);
}


//
// Create a new phidget manger and connect to the identified remote server
// by it's IP address.
//
PhidgetManager::PhidgetManager(MonkeyRuntime *rt, const char *address, int port, const char *password)
{
	devices = NULL;
	_serverID = NULL;

	if (CPhidgetManager_create(&managerHandle) != 0)
		throw std::runtime_error("Could not create a new phidget manager.");
	
	if (CPhidgetManager_openRemoteIP(managerHandle, address, (port == -1 ? 5001 : port), password) != 0)
		throw std::runtime_error("Could not connect to the phidget manager.");

	_address = new std::string(address);
	_password = (password != NULL ? new std::string(password) : NULL);
	_port = port;
	
	CommonConstruct(rt);
}


//
// Clean up the phidget manager and free all memory.
//
PhidgetManager::~PhidgetManager()
{
	CPhidgetManager_set_OnAttach_Handler(managerHandle, NULL, this);
	CPhidgetManager_set_OnDetach_Handler(managerHandle, NULL, this);

	if (devices != NULL)
	{
		while (devices->size() > 0)
		{
			Phidget *phidget = *devices->begin();
			
			devices->remove(phidget);
			delete phidget;
		}

		delete devices;
	}

	CPhidgetManager_close(managerHandle);
	CPhidgetManager_delete(managerHandle);
}


//
// Common constructor code that is called no matter how we were created.
//
void PhidgetManager::CommonConstruct(MonkeyRuntime *rt)
{
	devices = new std::list<Phidget *>();

	CPhidgetManager_set_OnAttach_Handler(managerHandle, Attached_Handler, this);
	CPhidgetManager_set_OnDetach_Handler(managerHandle, Detached_Handler, this);
	
	//
	// Prepare the scripting context.
	//
	translateContext = rt->CreateContext();

	//
	// Briefly pause executing to wait for things to settle. 500ms.
	//
	usleep(500000);
}


//
// Opens a phidget in whatever method is needed, optionally open a specific serial
// numbered phidget (recommended).
//
bool PhidgetManager::OpenPhidgetHandle(CPhidgetHandle handle, int serial)
{
	int error = -1;
	
	
	if (_serverID != NULL)
	{
		error = CPhidget_openRemote(handle, serial, _serverID->c_str(), (_password != NULL ? _password->c_str() : NULL));
	}
	else if (_address != NULL)
	{
		error = CPhidget_openRemoteIP(handle, serial, _address->c_str(), (_port != -1 ? _port : 5001), (_password != NULL ? _password->c_str() : NULL));
	}
	else
	{
		error = CPhidget_open(handle, serial);
	}
	
	return (error == EPHIDGET_OK);
}


//
// A phidget has been attached to the system. Create the new phidget class to
// handle interacting with it.
//
void PhidgetManager::PhidgetDidAttach(CPhidgetHandle handle)
{
	try
	{
		devices->push_back(Phidget::ConstructPhidget(handle, this));
	}
	catch (std::exception e)
	{
		printf("Exception while trying to construct phidget.\r\n");
	}
}


//
// A phidget was detached from the system. Clean up any references to it.
//
void PhidgetManager::PhidgetDidDetach(CPhidgetHandle handle)
{
	std::list<Phidget *>::iterator it;
	Phidget *phidget;
	
	
	for (it = devices->begin(); it != devices->end(); it++)
	{
		if ((*it)->handle == handle)
		{
			phidget = *it;
			devices->remove(phidget);
			delete phidget;
			
			break;
		}
	}
}


//
// Retrieve a list of the attached devices.
//
const std::list<Phidget *> PhidgetManager::AttachedDevices()
{
	std::list<Phidget *> phidgets;
	
	
	//
	// Make a copy of the list so we don't deal with threading issues.
	//
	phidgets = std::list<Phidget *>(devices->begin(), devices->end());
	
	return phidgets;
}


//
// Find a specific phidget device given it's serial number.
//
Phidget *PhidgetManager::FindDeviceByID(int serial)
{
	std::list<Phidget *>::iterator it;
	
	
	for (it = devices->begin(); it != devices->end(); it++)
	{
		if ((*it)->SerialNumber() == serial)
			return *it;
	}
	
	return NULL;
}


//
// Register a new property alias for this system.
//
void PhidgetManager::RegisterPropertyAlias(PhidgetPropertyAlias alias)
{
	if (propertyAliases.find(alias.GetAlias()) != propertyAliases.end())
		throw std::runtime_error("Alias already exists.");

	propertyAliases[alias.GetAlias()] = alias;
}


//
// Parse out the host, device and property name from the given property string.
// Returns the new property name as the function result and the hostID and deviceID
// by reference in the parameter list.
//
std::string PhidgetManager::ParsePropertyName(std::string property, std::string &hostID, Phidget **device, std::string &translator)
{
	std::map<std::string, PhidgetPropertyAlias>::iterator it;
	size_t index;
	

	//
	// Sane defaults.
	//	
	hostID = std::string();
	*device = NULL;
	translator = std::string();

	//
	// Determine the hostID, if any. We cannot reference a remote device directly so
	// only try to parse a deviceID if no host delimiter was found.
	//
	if ((index = property.find(':')) != std::string::npos)
	{
		hostID = property.substr(0, index);
		property = property.substr(index + 1);
	}
	else if ((index = property.find('.')) != std::string::npos)
	{
		std::string deviceID = property.substr(0, index);

		*device = FindDeviceByID(atoi(deviceID.c_str()));
		property = property.substr(index + 1);
	}
	else
	{
		//
		// Deal with an alias.
		//
		it = propertyAliases.find(property);
		if (it != propertyAliases.end())
		{
			*device = FindDeviceByID(it->second.GetDeviceSerial());
			translator = it->second.GetTranslator();
			property = it->second.GetProperty();
		}
	}

	return property;
}


//
// We are being told by a phidget that one of it's values has changed. Figure out
// which alias(es) that value is for and then notify the system of that new
// value.
//
// Notifications currently use the format "phidgetValue_<alias>" and have a userInfo
// key of "newValue" which contains the string representation of the new value.
//
void PhidgetManager::PhidgetValueChanged(Phidget *phidget, std::string valueName, std::string newValue)
{
	std::map<std::string, PhidgetPropertyAlias>::iterator it;
	int serial = phidget->SerialNumber();
	
	
	//
	// Look through all aliases and check each one to see if we need to send
	// a notification about it.
	//
	for (it = propertyAliases.begin(); it != propertyAliases.end(); it++)
	{
		//
		//
		if ((*it).second.GetDeviceSerial() == serial && (*it).second.GetProperty() == valueName)
		{
			MessageUserInfo info = MessageUserInfo();
			char name[128];
			
			snprintf(name, 128, "phidgetValue_%s", (*it).second.GetAlias().c_str());
			info["newValue"] = GetPropertyValue((*it).second.GetAlias());
			MessageCenter::SharedMessageCenter().PostMessage(Message(name, info));
		}
	}
}


//
// Get the property value of the named property, this can either be a direct
// reference or an alias'd name.
//
std::string PhidgetManager::GetPropertyValue(std::string property)
{
	std::string hostID, translator;
	Phidget *phidget;
	
	
	property = ParsePropertyName(property, hostID, &phidget, translator);

	//
	// Deal with retrieving a remote value.
	//
	if (hostID.size() > 0)
	{
		throw std::runtime_error("Remote points are not currently supported.");
	}
	else if (phidget != NULL)
	{
		std::string value;
		size_t index;
		
		//
		// Deal with a local property.
		//
		value = phidget->GetPropertyValue(property);

		//
		// If there is a translator for this value, translate it.
		//		
		if (translator.size() > 0)
		{
			for (index = translator.find("value"); index != std::string::npos; index = translator.find("value"))
			{
				translator.replace(index, 5, value);
			}

			return translateContext->EvaluateScript(translator.c_str());
		}
		else
			return value;
	}
	
	return std::string();
}


//
// Set the property value of the named property.
//
void PhidgetManager::SetPropertyValue(std::string property, std::string value)
{
	std::string hostID, translator;
	Phidget *phidget;
	
	
	property = ParsePropertyName(property, hostID, &phidget, translator);

	//
	// Deal with retrieving a remote value.
	//
	if (hostID.size() > 0)
	{
		throw std::runtime_error("Remote points are not currently supported.");
	}
	else if (phidget != NULL)
	{
		//
		// Deal with a local property.
		//
		return phidget->SetPropertyValue(property, value);
	}
}

